/*
 * Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */
package javax.swing.text;

import java.lang.reflect.*;
import java.text.*;
import java.util.*;
import sun.reflect.misc.ReflectUtil;
import sun.swing.SwingUtilities2;

/**
 * <code>NumberFormatter</code> subclasses <code>InternationalFormatter</code>
 * adding special behavior for numbers. Among the specializations are
 * (these are only used if the <code>NumberFormatter</code> does not display
 * invalid numbers, for example, <code>setAllowsInvalid(false)</code>):
 * <ul>
 * <li>Pressing +/- (- is determined from the
 * <code>DecimalFormatSymbols</code> associated with the
 * <code>DecimalFormat</code>) in any field but the exponent
 * field will attempt to change the sign of the number to
 * positive/negative.
 * <li>Pressing +/- (- is determined from the
 * <code>DecimalFormatSymbols</code> associated with the
 * <code>DecimalFormat</code>) in the exponent field will
 * attempt to change the sign of the exponent to positive/negative.
 * </ul>
 * <p>
 * If you are displaying scientific numbers, you may wish to turn on
 * overwrite mode, <code>setOverwriteMode(true)</code>. For example:
 * <pre>
 * DecimalFormat decimalFormat = new DecimalFormat("0.000E0");
 * NumberFormatter textFormatter = new NumberFormatter(decimalFormat);
 * textFormatter.setOverwriteMode(true);
 * textFormatter.setAllowsInvalid(false);
 * </pre>
 * <p>
 * If you are going to allow the user to enter decimal
 * values, you should either force the DecimalFormat to contain at least
 * one decimal (<code>#.0###</code>), or allow the value to be invalid
 * <code>setAllowsInvalid(true)</code>. Otherwise users may not be able to
 * input decimal values.
 * <p>
 * <code>NumberFormatter</code> provides slightly different behavior to
 * <code>stringToValue</code> than that of its superclass. If you have
 * specified a Class for values, {@link #setValueClass}, that is one of
 * of <code>Integer</code>, <code>Long</code>, <code>Float</code>,
 * <code>Double</code>, <code>Byte</code> or <code>Short</code> and
 * the Format's <code>parseObject</code> returns an instance of
 * <code>Number</code>, the corresponding instance of the value class
 * will be created using the constructor appropriate for the primitive
 * type the value class represents. For example:
 * <code>setValueClass(Integer.class)</code> will cause the resulting
 * value to be created via
 * <code>new Integer(((Number)formatter.parseObject(string)).intValue())</code>.
 * This is typically useful if you
 * wish to set a min/max value as the various <code>Number</code>
 * implementations are generally not comparable to each other. This is also
 * useful if for some reason you need a specific <code>Number</code>
 * implementation for your values.
 * <p>
 * <strong>Warning:</strong>
 * Serialized objects of this class will not be compatible with
 * future Swing releases. The current serialization support is
 * appropriate for short term storage or RMI between applications running
 * the same version of Swing.  As of 1.4, support for long term storage
 * of all JavaBeans&trade;
 * has been added to the <code>java.beans</code> package.
 * Please see {@link java.beans.XMLEncoder}.
 *
 * @since 1.4
 */
public class NumberFormatter extends InternationalFormatter {

  /**
   * The special characters from the Format instance.
   */
  private String specialChars;

  /**
   * Creates a <code>NumberFormatter</code> with the a default
   * <code>NumberFormat</code> instance obtained from
   * <code>NumberFormat.getNumberInstance()</code>.
   */
  public NumberFormatter() {
    this(NumberFormat.getNumberInstance());
  }

  /**
   * Creates a NumberFormatter with the specified Format instance.
   *
   * @param format Format used to dictate legal values
   */
  public NumberFormatter(NumberFormat format) {
    super(format);
    setFormat(format);
    setAllowsInvalid(true);
    setCommitsOnValidEdit(false);
    setOverwriteMode(false);
  }

  /**
   * Sets the format that dictates the legal values that can be edited
   * and displayed.
   * <p>
   * If you have used the nullary constructor the value of this property
   * will be determined for the current locale by way of the
   * <code>NumberFormat.getNumberInstance()</code> method.
   *
   * @param format NumberFormat instance used to dictate legal values
   */
  public void setFormat(Format format) {
    super.setFormat(format);

    DecimalFormatSymbols dfs = getDecimalFormatSymbols();

    if (dfs != null) {
      StringBuilder sb = new StringBuilder();

      sb.append(dfs.getCurrencySymbol());
      sb.append(dfs.getDecimalSeparator());
      sb.append(dfs.getGroupingSeparator());
      sb.append(dfs.getInfinity());
      sb.append(dfs.getInternationalCurrencySymbol());
      sb.append(dfs.getMinusSign());
      sb.append(dfs.getMonetaryDecimalSeparator());
      sb.append(dfs.getNaN());
      sb.append(dfs.getPercent());
      sb.append('+');
      specialChars = sb.toString();
    } else {
      specialChars = "";
    }
  }

  /**
   * Invokes <code>parseObject</code> on <code>f</code>, returning
   * its value.
   */
  Object stringToValue(String text, Format f) throws ParseException {
    if (f == null) {
      return text;
    }
    Object value = f.parseObject(text);

    return convertValueToValueClass(value, getValueClass());
  }

  /**
   * Converts the passed in value to the passed in class. This only
   * works if <code>valueClass</code> is one of <code>Integer</code>,
   * <code>Long</code>, <code>Float</code>, <code>Double</code>,
   * <code>Byte</code> or <code>Short</code> and <code>value</code>
   * is an instanceof <code>Number</code>.
   */
  private Object convertValueToValueClass(Object value, Class valueClass) {
    if (valueClass != null && (value instanceof Number)) {
      Number numberValue = (Number) value;
      if (valueClass == Integer.class) {
        return Integer.valueOf(numberValue.intValue());
      } else if (valueClass == Long.class) {
        return Long.valueOf(numberValue.longValue());
      } else if (valueClass == Float.class) {
        return Float.valueOf(numberValue.floatValue());
      } else if (valueClass == Double.class) {
        return Double.valueOf(numberValue.doubleValue());
      } else if (valueClass == Byte.class) {
        return Byte.valueOf(numberValue.byteValue());
      } else if (valueClass == Short.class) {
        return Short.valueOf(numberValue.shortValue());
      }
    }
    return value;
  }

  /**
   * Returns the character that is used to toggle to positive values.
   */
  private char getPositiveSign() {
    return '+';
  }

  /**
   * Returns the character that is used to toggle to negative values.
   */
  private char getMinusSign() {
    DecimalFormatSymbols dfs = getDecimalFormatSymbols();

    if (dfs != null) {
      return dfs.getMinusSign();
    }
    return '-';
  }

  /**
   * Returns the character that is used to toggle to negative values.
   */
  private char getDecimalSeparator() {
    DecimalFormatSymbols dfs = getDecimalFormatSymbols();

    if (dfs != null) {
      return dfs.getDecimalSeparator();
    }
    return '.';
  }

  /**
   * Returns the DecimalFormatSymbols from the Format instance.
   */
  private DecimalFormatSymbols getDecimalFormatSymbols() {
    Format f = getFormat();

    if (f instanceof DecimalFormat) {
      return ((DecimalFormat) f).getDecimalFormatSymbols();
    }
    return null;
  }

  /**
   * Subclassed to return false if <code>text</code> contains in an invalid
   * character to insert, that is, it is not a digit
   * (<code>Character.isDigit()</code>) and
   * not one of the characters defined by the DecimalFormatSymbols.
   */
  boolean isLegalInsertText(String text) {
    if (getAllowsInvalid()) {
      return true;
    }
    for (int counter = text.length() - 1; counter >= 0; counter--) {
      char aChar = text.charAt(counter);

      if (!Character.isDigit(aChar) &&
          specialChars.indexOf(aChar) == -1) {
        return false;
      }
    }
    return true;
  }

  /**
   * Subclassed to treat the decimal separator, grouping separator,
   * exponent symbol, percent, permille, currency and sign as literals.
   */
  boolean isLiteral(Map attrs) {
    if (!super.isLiteral(attrs)) {
      if (attrs == null) {
        return false;
      }
      int size = attrs.size();

      if (attrs.get(NumberFormat.Field.GROUPING_SEPARATOR) != null) {
        size--;
        if (attrs.get(NumberFormat.Field.INTEGER) != null) {
          size--;
        }
      }
      if (attrs.get(NumberFormat.Field.EXPONENT_SYMBOL) != null) {
        size--;
      }
      if (attrs.get(NumberFormat.Field.PERCENT) != null) {
        size--;
      }
      if (attrs.get(NumberFormat.Field.PERMILLE) != null) {
        size--;
      }
      if (attrs.get(NumberFormat.Field.CURRENCY) != null) {
        size--;
      }
      if (attrs.get(NumberFormat.Field.SIGN) != null) {
        size--;
      }
      return size == 0;
    }
    return true;
  }

  /**
   * Subclassed to make the decimal separator navigable, as well
   * as making the character between the integer field and the next
   * field navigable.
   */
  boolean isNavigatable(int index) {
    if (!super.isNavigatable(index)) {
      // Don't skip the decimal, it causes wierd behavior
      return getBufferedChar(index) == getDecimalSeparator();
    }
    return true;
  }

  /**
   * Returns the first <code>NumberFormat.Field</code> starting
   * <code>index</code> incrementing by <code>direction</code>.
   */
  private NumberFormat.Field getFieldFrom(int index, int direction) {
    if (isValidMask()) {
      int max = getFormattedTextField().getDocument().getLength();
      AttributedCharacterIterator iterator = getIterator();

      if (index >= max) {
        index += direction;
      }
      while (index >= 0 && index < max) {
        iterator.setIndex(index);

        Map attrs = iterator.getAttributes();

        if (attrs != null && attrs.size() > 0) {
          for (Object key : attrs.keySet()) {
            if (key instanceof NumberFormat.Field) {
              return (NumberFormat.Field) key;
            }
          }
        }
        index += direction;
      }
    }
    return null;
  }

  /**
   * Overriden to toggle the value if the positive/minus sign
   * is inserted.
   */
  void replace(DocumentFilter.FilterBypass fb, int offset, int length,
      String string, AttributeSet attr) throws BadLocationException {
    if (!getAllowsInvalid() && length == 0 && string != null &&
        string.length() == 1 &&
        toggleSignIfNecessary(fb, offset, string.charAt(0))) {
      return;
    }
    super.replace(fb, offset, length, string, attr);
  }

  /**
   * Will change the sign of the integer or exponent field if
   * <code>aChar</code> is the positive or minus sign. Returns
   * true if a sign change was attempted.
   */
  private boolean toggleSignIfNecessary(DocumentFilter.FilterBypass fb,
      int offset, char aChar) throws
      BadLocationException {
    if (aChar == getMinusSign() || aChar == getPositiveSign()) {
      NumberFormat.Field field = getFieldFrom(offset, -1);
      Object newValue;

      try {
        if (field == null ||
            (field != NumberFormat.Field.EXPONENT &&
                field != NumberFormat.Field.EXPONENT_SYMBOL &&
                field != NumberFormat.Field.EXPONENT_SIGN)) {
          newValue = toggleSign((aChar == getPositiveSign()));
        } else {
          // exponent
          newValue = toggleExponentSign(offset, aChar);
        }
        if (newValue != null && isValidValue(newValue, false)) {
          int lc = getLiteralCountTo(offset);
          String string = valueToString(newValue);

          fb.remove(0, fb.getDocument().getLength());
          fb.insertString(0, string, null);
          updateValue(newValue);
          repositionCursor(getLiteralCountTo(offset) -
              lc + offset, 1);
          return true;
        }
      } catch (ParseException pe) {
        invalidEdit();
      }
    }
    return false;
  }

  /**
   * Invoked to toggle the sign. For this to work the value class
   * must have a single arg constructor that takes a String.
   */
  private Object toggleSign(boolean positive) throws ParseException {
    Object value = stringToValue(getFormattedTextField().getText());

    if (value != null) {
      // toString isn't localized, so that using +/- should work
      // correctly.
      String string = value.toString();

      if (string != null && string.length() > 0) {
        if (positive) {
          if (string.charAt(0) == '-') {
            string = string.substring(1);
          }
        } else {
          if (string.charAt(0) == '+') {
            string = string.substring(1);
          }
          if (string.length() > 0 && string.charAt(0) != '-') {
            string = "-" + string;
          }
        }
        if (string != null) {
          Class<?> valueClass = getValueClass();

          if (valueClass == null) {
            valueClass = value.getClass();
          }
          try {
            ReflectUtil.checkPackageAccess(valueClass);
            SwingUtilities2.checkAccess(valueClass.getModifiers());
            Constructor cons = valueClass.getConstructor(
                new Class[]{String.class});
            if (cons != null) {
              SwingUtilities2.checkAccess(cons.getModifiers());
              return cons.newInstance(new Object[]{string});
            }
          } catch (Throwable ex) {
          }
        }
      }
    }
    return null;
  }

  /**
   * Invoked to toggle the sign of the exponent (for scientific
   * numbers).
   */
  private Object toggleExponentSign(int offset, char aChar) throws
      BadLocationException, ParseException {
    String string = getFormattedTextField().getText();
    int replaceLength = 0;
    int loc = getAttributeStart(NumberFormat.Field.EXPONENT_SIGN);

    if (loc >= 0) {
      replaceLength = 1;
      offset = loc;
    }
    if (aChar == getPositiveSign()) {
      string = getReplaceString(offset, replaceLength, null);
    } else {
      string = getReplaceString(offset, replaceLength,
          new String(new char[]{aChar}));
    }
    return stringToValue(string);
  }
}
